<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>声纹识别系统</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lamejs@1.2.0/lame.all.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <style>
        .container { max-width: 800px; margin-top: 30px; }
        .card { margin-bottom: 20px; border: 1px solid #ddd; }
        .card-header { background-color: #f8f9fa; }
        .result { margin-top: 20px; padding: 15px; border-radius: 5px; transition: all 0.3s; }
        .success { background-color: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; }
        .error { background-color: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
        .warning { background-color: #fff3cd; color: #664d03; border: 1px solid #ffecb5; }
        .recorder-container { margin-top: 15px; }
        .recorder-controls { display: flex; gap: 10px; margin-bottom: 10px; }
        .audio-preview { margin-top: 10px; padding: 15px; background-color: #f8f9fa; border-radius: 5px; }
        .btn-record { background-color: #dc3545; color: white; }
        .btn-record:disabled { background-color: #bb2d3b; opacity: 0.7; }
        .btn-stop { background-color: #ffc107; color: black; }
        .btn-stop:disabled { background-color: #ffc107; opacity: 0.7; }
        .btn-send { background-color: #198754; color: white; }
        .btn-send:disabled { background-color: #157347; opacity: 0.7; }
        .recording-indicator { display: inline-block; width: 10px; height: 10px; background-color: #dc3545; border-radius: 50%; margin-right: 5px; animation: pulse 1.5s infinite; }
        .hint { font-size: 0.85rem; color: #6c757d; margin-top: 5px; padding: 8px; background-color: #f8f9fa; border-radius: 4px; }
        .action-guide { margin-top: 10px; font-size: 0.9rem; padding: 10px; border-radius: 4px; }
        .guide-success { background-color: #e6f7ee; color: #087f5b; }
        .guide-error { background-color: #fff5f5; color: #c92a2a; }
        @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
        .animate-shake { animation: shake 0.5s ease-in-out; }
        @keyframes shake {
            0%, 100% { transform: translateX(0); }
            10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
            20%, 40%, 60%, 80% { transform: translateX(5px); }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1 class="text-center mb-4">声纹识别系统</h1>

        <!-- 1. 创建分组 -->
        <div class="card">
            <div class="card-header">
                <h5>1. 创建声纹分组</h5>
            </div>
            <div class="card-body">
                <div class="mb-3">
                    <label for="group_id" class="form-label">分组ID（可选）</label>
                    <input type="text" class="form-control" id="group_id" placeholder="默认: my_group_001">
                </div>
                <button onclick="createGroup()" class="btn btn-primary">创建分组</button>
                <div id="group_result" class="result d-none"></div>
                <div id="group_guide" class="action-guide d-none"></div>
            </div>
        </div>

        <!-- 2. 注册声纹 -->
        <div class="card">
            <div class="card-header">
                <h5>2. 注册声纹特征</h5>
            </div>
            <div class="card-body">
                <div class="mb-3">
                    <label for="reg_feature_id" class="form-label">用户ID（必填）</label>
                    <input type="text" class="form-control" id="reg_feature_id" placeholder="例如: user_001">
                </div>
                <div class="mb-3">
                    <label for="reg_group_id" class="form-label">分组ID（可选）</label>
                    <input type="text" class="form-control" id="reg_group_id" placeholder="默认: my_group_001">
                </div>
                
                <!-- 录音控件 -->
                <div class="recorder-container">
                    <div class="recorder-controls">
                        <button id="reg_startBtn" class="btn btn-record" onclick="startRecording('reg')">
                            <i class="fa fa-microphone"></i> 开始录音
                        </button>
                        <button id="reg_stopBtn" class="btn btn-stop" onclick="stopRecording('reg')" disabled>
                            <i class="fa fa-stop"></i> 停止录音
                        </button>
                        <button id="reg_sendBtn" class="btn btn-send" onclick="registerFeature()" disabled>
                            <i class="fa fa-paper-plane"></i> 发送录音
                        </button>
                    </div>
                    <div id="reg_recordingStatus" class="text-muted mb-2 d-none">
                        <span class="recording-indicator"></span>正在录音...请对着麦克风说话（建议3-5秒，如"您好，我是XXX"）
                    </div>
                    <div id="reg_audioPreview" class="audio-preview d-none">
                        <h6 class="mb-2">录音预览（可播放）</h6>
                        <audio id="reg_audioPlayer" controls class="w-full mb-2"></audio>
                        <div class="d-flex justify-content-between text-sm text-muted">
                            <span>格式: MP3</span>
                            <span>采样率: 16kHz | 位长: 16bit | 单声道</span>
                        </div>
                    </div>
                    <div class="hint">
                        <i class="fa fa-info-circle"></i> 提示：请在安静环境下录制，避免背景噪音；语音内容清晰，长度3-5秒最佳
                    </div>
                </div>
                
                <div id="reg_result" class="result d-none"></div>
                <div id="reg_guide" class="action-guide d-none"></div>
            </div>
        </div>

        <!-- 3. 声纹验证 -->
        <div class="card">
            <div class="card-header">
                <h5>3. 1对1声纹验证</h5>
            </div>
            <div class="card-body">
                <div class="mb-3">
                    <label for="verify_feature_id" class="form-label">用户ID（必填）</label>
                    <input type="text" class="form-control" id="verify_feature_id" placeholder="例如: user_001">
                </div>
                <div class="mb-3">
                    <label for="verify_group_id" class="form-label">分组ID（可选）</label>
                    <input type="text" class="form-control" id="verify_group_id" placeholder="默认: my_group_001">
                </div>
                <div class="mb-3">
                    <label for="threshold" class="form-label">相似度阈值（可选）</label>
                    <input type="number" step="0.01" min="0" max="1" class="form-control" id="threshold" value="0.8">
                </div>
                
                <!-- 录音控件 -->
                <div class="recorder-container">
                    <div class="recorder-controls">
                        <button id="verify_startBtn" class="btn btn-record" onclick="startRecording('verify')">
                            <i class="fa fa-microphone"></i> 开始录音
                        </button>
                        <button id="verify_stopBtn" class="btn btn-stop" onclick="stopRecording('verify')" disabled>
                            <i class="fa fa-stop"></i> 停止录音
                        </button>
                        <button id="verify_sendBtn" class="btn btn-send" onclick="verifyFeature()" disabled>
                            <i class="fa fa-paper-plane"></i> 发送录音
                        </button>
                    </div>
                    <div id="verify_recordingStatus" class="text-muted mb-2 d-none">
                        <span class="recording-indicator"></span>正在录音...请对着麦克风说话（建议3-5秒，与注册内容相似）
                    </div>
                    <div id="verify_audioPreview" class="audio-preview d-none">
                        <h6 class="mb-2">录音预览（可播放）</h6>
                        <audio id="verify_audioPlayer" controls class="w-full mb-2"></audio>
                        <div class="d-flex justify-content-between text-sm text-muted">
                            <span>格式: MP3</span>
                            <span>采样率: 16kHz | 位长: 16bit | 单声道</span>
                        </div>
                    </div>
                    <div class="hint">
                        <i class="fa fa-info-circle"></i> 提示：请使用与注册时相似的语气和内容（如相同的句子），以提高验证准确率
                    </div>
                </div>
                
                <div id="verify_result" class="result d-none"></div>
                <div id="verify_guide" class="action-guide d-none"></div>
            </div>
        </div>
    </div>

    <script>
        // 全局变量
        let audioContext;
        let mediaRecorder;
        let audioChunks = [];
        let currentType = null;
        const API_BASE_URL = 'http://localhost:5008/api';

        // 初始化音频上下文
        function initAudioContext() {
            if (!audioContext) {
                try {
                    audioContext = new (window.AudioContext || window.webkitAudioContext)();
                    console.log(`音频上下文初始化成功，采样率: ${audioContext.sampleRate}`);
                } catch (error) {
                    console.error('无法初始化 AudioContext:', error);
                    showResult('reg_result', 'error', '您的浏览器不支持音频录制功能，请更换浏览器');
                    showActionGuide('reg_guide', 'error', '建议使用Chrome、Firefox等现代浏览器');
                    showResult('verify_result', 'error', '您的浏览器不支持音频录制功能，请更换浏览器');
                    showActionGuide('verify_guide', 'error', '建议使用Chrome、Firefox等现代浏览器');
                }
            }
        }

        // 开始录音
        function startRecording(type) {
            // 隐藏之前的结果和引导
            hideResultAndGuide(type);
            
            initAudioContext();
            currentType = type;
            audioChunks = [];
            
            // 更新UI状态
            document.getElementById(`${type}_recordingStatus`).classList.remove('d-none');
            document.getElementById(`${type}_audioPreview`).classList.add('d-none');
            document.getElementById(`${type}_startBtn`).disabled = true;
            document.getElementById(`${type}_stopBtn`).disabled = false;
            document.getElementById(`${type}_sendBtn`).disabled = true;

            // 请求麦克风权限
            navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000 } })
                .then(stream => {
                    mediaRecorder = new MediaRecorder(stream);
                    mediaRecorder.addEventListener('dataavailable', event => {
                        audioChunks.push(event.data);
                    });
                    mediaRecorder.addEventListener('stop', () => {
                        const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
                        console.log(`录音完成，原始音频大小: ${audioBlob.size} bytes`);
                        processAudio(audioBlob, type);
                        stream.getTracks().forEach(track => track.stop());
                    });
                    mediaRecorder.start();
                    console.log(`开始录音: ${type}`);
                })
                .catch(error => {
                    console.error('录音错误:', error);
                    const errorMsg = error.name === 'NotAllowedError' 
                        ? '未获得麦克风权限，请在浏览器设置中允许麦克风访问'
                        : `录音失败: ${error.message}`;
                    showResult(`${type}_result`, 'error', errorMsg);
                    showActionGuide(`${type}_guide`, 'error', '请检查麦克风是否可用并授予权限后重试');
                    resetRecordingState(type, true); // 失败后允许重新录音
                });
        }

        // 停止录音
        function stopRecording(type) {
            if (mediaRecorder && mediaRecorder.state !== 'inactive') {
                mediaRecorder.stop();
                document.getElementById(`${type}_recordingStatus`).classList.add('d-none');
                document.getElementById(`${type}_stopBtn`).disabled = true;
                console.log(`停止录音: ${type}`);
            }
        }

        // 处理音频（转换为符合API要求的格式）
        function processAudio(audioBlob, type) {
            showResult(`${type}_result`, 'warning', '正在处理音频...请稍候');
            
            convertToMp3(audioBlob)
                .then(convertedBlob => {
                    console.log(`音频转换完成，MP3大小: ${convertedBlob.size} bytes`);
                    
                    // 检查音频长度是否足够
                    if (convertedBlob.size < 5 * 1024) { // 小于5KB视为过短
                        showResult(`${type}_result`, 'error', '音频长度过短（建议3-5秒）');
                        showActionGuide(`${type}_guide`, 'error', '请点击"开始录音"重新录制更长的语音');
                        resetRecordingState(type, true);
                        return;
                    }
                    
                    // 创建音频预览
                    const audioUrl = URL.createObjectURL(convertedBlob);
                    const audioPlayer = document.getElementById(`${type}_audioPlayer`);
                    audioPlayer.src = audioUrl;
                    
                    // 显示预览并启用发送按钮
                    document.getElementById(`${type}_audioPreview`).classList.remove('d-none');
                    document.getElementById(`${type}_sendBtn`).disabled = false;
                    window[`${type}AudioBlob`] = convertedBlob;
                    
                    // 隐藏处理提示
                    document.getElementById(`${type}_result`).classList.add('d-none');
                    showActionGuide(`${type}_guide`, 'success', '音频处理完成，请点击"发送录音"提交');
                })
                .catch(error => {
                    console.error('音频处理错误:', error);
                    showResult(`${type}_result`, 'error', `音频处理失败: ${error.message}`);
                    showActionGuide(`${type}_guide`, 'error', '请点击"开始录音"重新尝试');
                    resetRecordingState(type, true);
                });
        }

        // 转换为16kHz单声道MP3（严格符合API要求）
        function convertToMp3(audioBlob) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = function(event) {
                    const arrayBuffer = event.target.result;
                    const targetSampleRate = 16000;
                    
                    const audioContext = new (window.AudioContext || window.webkitAudioContext)();
                    audioContext.decodeAudioData(arrayBuffer)
                        .then(audioBuffer => {
                            console.log(`原始音频信息: 采样率=${audioBuffer.sampleRate}, 声道=${audioBuffer.numberOfChannels}`);
                            
                            // 转为单声道
                            const monoData = getMonoAudioData(audioBuffer);
                            
                            // 重采样为16kHz
                            const resampledData = resample(monoData, audioBuffer.sampleRate, targetSampleRate);
                            
                            // 转换为16bit PCM
                            const int16Data = floatTo16BitPCM(resampledData);
                            
                            // 编码为MP3（lame编码）
                            const mp3Blob = encodeToMp3(int16Data, targetSampleRate);
                            resolve(mp3Blob);
                        })
                        .catch(err => reject(`解码失败: ${err.message}`));
                };
                reader.onerror = () => reject(`文件读取失败: ${reader.error.message}`);
                reader.readAsArrayBuffer(audioBlob);
            });
        }

        // 转为单声道
        function getMonoAudioData(audioBuffer) {
            if (audioBuffer.numberOfChannels === 1) {
                return audioBuffer.getChannelData(0);
            }
            // 合并双声道为单声道
            const left = audioBuffer.getChannelData(0);
            const right = audioBuffer.getChannelData(1);
            const mono = new Float32Array(left.length);
            for (let i = 0; i < left.length; i++) {
                mono[i] = (left[i] + right[i]) / 2;
            }
            return mono;
        }

        // 重采样（确保16kHz）
        function resample(data, sourceRate, targetRate) {
            if (sourceRate === targetRate) return data;
            
            const ratio = sourceRate / targetRate;
            const newLength = Math.floor(data.length / ratio);
            const resampled = new Float32Array(newLength);
            
            for (let i = 0; i < newLength; i++) {
                const index = Math.floor(i * ratio);
                resampled[i] = data[index];
            }
            console.log(`重采样完成: ${sourceRate} -> ${targetRate}, 长度: ${newLength}`);
            return resampled;
        }

        // Float32转16bit PCM
        function floatTo16BitPCM(input) {
            const buffer = new ArrayBuffer(input.length * 2);
            const view = new DataView(buffer);
            for (let i = 0; i < input.length; i++) {
                const value = Math.max(-1, Math.min(1, input[i]));
                view.setInt16(i * 2, value < 0 ? value * 0x8000 : value * 0x7FFF, true);
            }
            return new Int16Array(buffer);
        }

        // 编码为MP3（lame编码）
        function encodeToMp3(int16Data, sampleRate) {
            const mp3encoder = new lamejs.Mp3Encoder(1, sampleRate, 128);
            const buffer = [];
            const blockSize = 5760;
            
            for (let i = 0; i < int16Data.length; i += blockSize) {
                const chunk = int16Data.subarray(i, i + blockSize);
                const mp3Data = mp3encoder.encodeBuffer(chunk);
                if (mp3Data.length > 0) buffer.push(mp3Data);
            }
            const finalData = mp3encoder.flush();
            if (finalData.length > 0) buffer.push(finalData);
            
            return new Blob(buffer, { type: 'audio/mp3' });
        }

        // 创建分组
        function createGroup() {
            hideResultAndGuide('group');
            
            const groupId = document.getElementById('group_id').value.trim() || 'my_group_001';
            showResult('group_result', 'warning', `正在创建分组 "${groupId}"...`);
            
            fetch(`${API_BASE_URL}/create-group`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ group_id: groupId })
            })
            .then(response => handleResponse(response, 'group', (data) => {
                if (data.status === 'success') {
                    showResult('group_result', 'success', `分组 "${groupId}" 创建成功`);
                    showActionGuide('group_guide', 'success', '请继续进行声纹注册操作');
                } else if (data.status === 'warning') {
                    showResult('group_result', 'warning', data.message);
                    showActionGuide('group_guide', 'success', '可直接使用该分组进行声纹注册');
                }
            }))
            .catch(error => {
                showResult('group_result', 'error', `创建分组失败: ${error.message}`);
                showActionGuide('group_guide', 'error', '请检查网络后重新点击"创建分组"按钮');
            });
        }

        // 注册声纹特征
        function registerFeature() {
            hideResultAndGuide('reg');
            
            const featureId = document.getElementById('reg_feature_id').value.trim();
            const groupId = document.getElementById('reg_group_id').value.trim() || 'my_group_001';
            const audioBlob = window['regAudioBlob'];
            
            // 前端校验
            if (!featureId) {
                showResult('reg_result', 'error', '请输入用户ID（例如：user_001）');
                showActionGuide('reg_guide', 'error', '请填写用户ID后再尝试');
                return;
            }
            if (!audioBlob) {
                showResult('reg_result', 'error', '请先录制音频');
                showActionGuide('reg_guide', 'error', '请点击"开始录音"录制语音');
                return;
            }
            
            showResult('reg_result', 'warning', `正在注册用户 "${featureId}" 的声纹特征...`);
            
            const formData = new FormData();
            formData.append('feature_id', featureId);
            formData.append('group_id', groupId);
            formData.append('audio', audioBlob, 'recording.mp3');
            
            fetch(`${API_BASE_URL}/register-feature`, {
                method: 'POST',
                body: formData
            })
            .then(response => handleResponse(response, 'reg', (data) => {
                if (data.status === 'success') {
                    showResult('reg_result', 'success', `用户 "${featureId}" 声纹注册成功`);
                    showActionGuide('reg_guide', 'success', '可进行声纹验证操作，或点击"开始录音"注册新的声纹');
                    // 成功后禁用发送按钮，但保留重新录音的可能
                    document.getElementById('reg_sendBtn').disabled = true;
                }
            }))
            .catch(error => {
                console.error('注册失败:', error);
                showResult('reg_result', 'error', `注册失败: ${error.message}`);
                
                // 根据错误类型显示不同的引导
                let guideMsg = '请点击"开始录音"重新录制并提交';
                if (error.message.includes('音频数据无效')) {
                    guideMsg = '请确保在安静环境下录制3-5秒清晰语音，然后重新提交';
                } else if (error.message.includes('权限')) {
                    guideMsg = '请检查API权限配置后重试';
                }
                showActionGuide('reg_guide', 'error', guideMsg);
                resetRecordingState('reg', true); // 失败后允许重新操作
            });
        }

        // 验证声纹特征
        function verifyFeature() {
            hideResultAndGuide('verify');
            
            const featureId = document.getElementById('verify_feature_id').value.trim();
            const groupId = document.getElementById('verify_group_id').value.trim() || 'my_group_001';
            const threshold = parseFloat(document.getElementById('threshold').value) || 0.8;
            const audioBlob = window['verifyAudioBlob'];
            
            // 前端校验
            if (!featureId) {
                showResult('verify_result', 'error', '请输入用户ID（例如：user_001）');
                showActionGuide('verify_guide', 'error', '请填写用户ID后再尝试');
                return;
            }
            if (!audioBlob) {
                showResult('verify_result', 'error', '请先录制音频');
                showActionGuide('verify_guide', 'error', '请点击"开始录音"录制语音');
                return;
            }
            
            showResult('verify_result', 'warning', `正在验证用户 "${featureId}" 的声纹...`);
            
            const formData = new FormData();
            formData.append('feature_id', featureId);
            formData.append('group_id', groupId);
            formData.append('threshold', threshold);
            formData.append('audio', audioBlob, 'recording.mp3');
            
            fetch(`${API_BASE_URL}/verify-feature`, {
                method: 'POST',
                body: formData
            })
            .then(response => handleResponse(response, 'verify', (data) => {
                if (data.status === 'success') {
                    const message = data.is_match 
                        ? `验证成功！声纹相似度为 ${data.score}（阈值: ${data.threshold}）`
                        : `验证失败！声纹相似度为 ${data.score}（阈值: ${data.threshold}）`;
                    showResult('verify_result', data.is_match ? 'success' : 'error', message);
                    
                    // 显示引导信息
                    if (data.is_match) {
                        showActionGuide('verify_guide', 'success', '验证通过，可点击"开始录音"进行再次验证');
                    } else {
                        showActionGuide('verify_guide', 'error', '请使用与注册时相同的语气和内容重新录制验证');
                        resetRecordingState('verify', true); // 验证失败允许重新操作
                    }
                    document.getElementById('verify_sendBtn').disabled = true;
                }
            }))
            .catch(error => {
                console.error('验证失败:', error);
                showResult('verify_result', 'error', `验证失败: ${error.message}`);
                
                let guideMsg = '请点击"开始录音"重新录制并提交';
                if (error.message.includes('未找到声纹')) {
                    guideMsg = '该用户未注册声纹，请先完成声纹注册';
                } else if (error.message.includes('音频数据无效')) {
                    guideMsg = '请确保录制3-5秒清晰语音，然后重新提交';
                }
                showActionGuide('verify_guide', 'error', guideMsg);
                resetRecordingState('verify', true);
            });
        }

        // 统一处理响应
        function handleResponse(response, type, successCallback) {
            return new Promise((resolve, reject) => {
                if (!response.ok) {
                    return response.text().then(text => {
                        try {
                            const error = JSON.parse(text);
                            // 解析常见错误码
                            if (error.message.includes('23003')) {
                                error.message = '音频数据无效（格式错误或过短）';
                            } else if (error.message.includes('23006')) {
                                error.message = '分组已存在';
                            } else if (error.message.includes('23010')) {
                                error.message = '未找到该用户的声纹特征（未注册）';
                            }
                            reject(new Error(error.message));
                        } catch (e) {
                            reject(new Error(`服务器错误: ${text}`));
                        }
                    });
                }
                
                response.json().then(data => {
                    successCallback(data);
                    resolve(data);
                });
            });
        }

        // 显示结果信息
        function showResult(elementId, type, message) {
            const resultElement = document.getElementById(elementId);
            resultElement.textContent = message;
            resultElement.classList.remove('success', 'error', 'warning', 'd-none', 'animate-shake');
            resultElement.classList.add(type);
            
            if (type === 'error') {
                resultElement.classList.add('animate-shake');
                setTimeout(() => resultElement.classList.remove('animate-shake'), 1000);
            }
        }

        // 显示操作引导
        function showActionGuide(elementId, type, message) {
            const guideElement = document.getElementById(elementId);
            guideElement.innerHTML = `<i class="fa ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'}"></i> ${message}`;
            guideElement.classList.remove('guide-success', 'guide-error', 'd-none');
            guideElement.classList.add(`guide-${type}`);
        }

        // 隐藏结果和引导
        function hideResultAndGuide(type) {
            document.getElementById(`${type}_result`).classList.add('d-none');
            document.getElementById(`${type}_guide`).classList.add('d-none');
        }

        // 重置录音状态
        function resetRecordingState(type, enableRetry) {
            document.getElementById(`${type}_startBtn`).disabled = !enableRetry;
            document.getElementById(`${type}_stopBtn`).disabled = true;
            document.getElementById(`${type}_sendBtn`).disabled = true;
            document.getElementById(`${type}_recordingStatus`).classList.add('d-none');
            // 保留音频预览，方便用户重新发送（如果需要）
        }
    </script>
</body>
</html>